Introduction into moveVis

1. Background

  • MoveViz is a package that consists of a set of tools used for visualizing movement data into animated trajectories

  • Movement trajectories used for ggplots

  • Spatial data containing x and y geolocation points for plots and maps is integrated with time stamp data (temporal data) forming spatio-temporal data

  • Move, raster class inputs or movestack data added to baselayer map for ggplot frames

  • MoveVis functions to animate movement trajectories using different time intervals

  • Frames rendered into animations encoded as GIF or video file,

2. Application of moveVis

  • Many of us in Biology programs conduct research consisting of track data for marine life such as sharks, dolphins and fish.

  • Migration of cattle, bears, birds, snakes, lizards and endangered species.

  • Population data i.e. human migration movement data across continents over time

  • Visually explore and interpreting movement patterns

  • Potential interactions of individuals with each other and their environment

  • Climate data, tornados, storms, rain & wind, tracking major weather events

  • Historical data, migration, climate, environmental

  • Stock data

  • Can be used to track sports races/events such as bike races, cars, marathons etc

  • Time resolution could be 500 years for human migration or 5 seconds for bike race data

3. The 4 computational steps

  • To produce a ggplot or video output containing animated movement data, 4 main functions must be carried out

    1. preparing data, (b) creating frames, (c) adapting frames and (d) animating frames
  • Each of which have sub functions within their category to further optimize the output features of the visual.

  • There are additional features that function by manipulating and changing the data such as those in the tidyverse package

———————————————————————————————-

Loading our Libraries

library(tidyverse)
library(here)
library(tidytuesdayR)
library(lubridate)

To Download moveVis : install.packages(“moveVis”)

# install.packages("moveVis")
library(moveVis)

Loading our Sample data

To demostrate the abilities of this package, we will use Pet Cats UK dataset from TidyTuesday

Will use the “cats_uk” dataset

tuesdata <- tidytuesdayR::tt_load(2023, week = 5)
## 
##  Downloading file 1 of 2: `cats_uk.csv`
##  Downloading file 2 of 2: `cats_uk_reference.csv`
cats_uk <- as.data.frame(tuesdata$cats_uk) 

Data Requirement

  • unique sample identifier column
  • time stamp (in a date format)
  • x (longitude)
  • y (latitude)
glimpse(cats_uk)
## Rows: 18,215
## Columns: 11
## $ tag_id                   <chr> "Ares", "Ares", "Ares", "Ares", "Ares", "Ares…
## $ event_id                 <dbl> 3395610551, 3395610552, 3395610553, 339561055…
## $ visible                  <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRU…
## $ timestamp                <dttm> 2017-06-24 01:03:57, 2017-06-24 01:11:20, 20…
## $ location_long            <dbl> -5.113851, -5.113851, -5.113730, -5.113774, -…
## $ location_lat             <dbl> 50.17032, 50.17032, 50.16988, 50.16983, 50.17…
## $ ground_speed             <dbl> 684, 936, 2340, 0, 4896, 504, 108, 504, 252, …
## $ height_above_ellipsoid   <dbl> 154.67, 154.67, 81.35, 67.82, 118.03, 123.07,…
## $ algorithm_marked_outlier <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ manually_marked_outlier  <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ study_name               <chr> "Pet Cats United Kingdom", "Pet Cats United K…

Preparing Movement Tracks

Cleaning the data

  • Needs to be a times data

  • Cannot have duplicated time stamp

cats_uk <- cats_uk%>% 
  mutate( timestamp = ymd_hms(timestamp)) %>%
  filter(tag_id %in% c("Athena","Ares", "Lola")) #simplify dataset to managable practice set

If there is duplicated timestamp, needs to be removed

cats_uk <- cats_uk[!duplicated(cats_uk$timestamp),]

Use a object called Move or moveStack

First have to convert a data.frame to move or moveStack using df2move()

Our required compnents of the data will be specified here

cat <- df2move(cats_uk,  #df
        proj = "+init=epsg:4326 +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0",  #type of projection
        x = "location_long",  #what are your x coord
        y = "location_lat",
        time = "timestamp", #what are the time, needs to be POSIXct
        track_id = "tag_id"
)

glimpse(cat)
## Formal class 'MoveStack' [package "move"] with 17 slots
##   ..@ trackId                : Factor w/ 3 levels "Ares","Athena",..: 1 1 1 1 1 1 1 1 1 1 ...
##   ..@ timestamps             : POSIXct[1:432], format: "2017-06-24 01:03:57" "2017-06-24 01:11:20" ...
##   ..@ idData                 :'data.frame':  3 obs. of  0 variables
##   ..@ sensor                 : Factor w/ 1 level "unknown": 1 1 1 1 1 1 1 1 1 1 ...
##   ..@ data                   :'data.frame':  432 obs. of  3 variables:
##   .. ..$ x   : num [1:432] -5.11 -5.11 -5.11 -5.11 -5.11 ...
##   .. ..$ y   : num [1:432] 50.2 50.2 50.2 50.2 50.2 ...
##   .. ..$ time: POSIXct[1:432], format: "2017-06-24 01:03:57" "2017-06-24 01:11:20" ...
##   ..@ coords.nrs             : num(0) 
##   ..@ coords                 : num [1:432, 1:2] -5.11 -5.11 -5.11 -5.11 -5.11 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   ..@ bbox                   : num [1:2, 1:2] -5.12 50.14 -5.07 50.17
##   .. ..- attr(*, "dimnames")=List of 2
##   ..@ proj4string            :Formal class 'CRS' [package "sp"] with 1 slot
##   ..@ trackIdUnUsedRecords   : Factor w/ 3 levels "Ares","Athena",..: 
##   ..@ timestampsUnUsedRecords: NULL
##   ..@ sensorUnUsedRecords    : Factor w/ 1 level "unknown": 
##   ..@ dataUnUsedRecords      :'data.frame':  0 obs. of  0 variables
##   ..@ dateCreation           : POSIXct[1:1], format: "2023-03-25 21:21:29"
##   ..@ study                  : chr(0) 
##   ..@ citation               : chr(0) 
##   ..@ license                : chr(0)

Align time resolution

To be able to convert data into frames, it needs to be consistent time intervals

m <- align_move(cat, 
                res = 10, #specify resolution
                unit = "mins") #resolution unit

glimpse(m)
## Formal class 'MoveStack' [package "move"] with 17 slots
##   ..@ trackId                : Factor w/ 3 levels "Ares","Athena",..: 1 1 1 1 1 1 1 1 1 1 ...
##   ..@ timestamps             : POSIXct[1:2903], format: "2017-06-24 01:10:13" "2017-06-24 01:20:13" ...
##   .. ..- attr(*, "names")="Ares1" "Ares2" ...
##   ..@ idData                 :'data.frame':  3 obs. of  0 variables
##   ..@ sensor                 : Factor w/ 2 levels "unknown","interpolateTime": 2 2 2 2 2 2 2 2 2 2 ...
##   .. ..- attr(*, "names")= chr [1:2903] "Ares1" "Ares2" "Ares3" "Ares4" ...
##   ..@ data                   :'data.frame':  2903 obs. of  3 variables:
##   .. ..$ x   : num [1:2903] -5.11 -5.11 -5.11 -5.11 -5.11 ...
##   .. ..$ y   : num [1:2903] 50.2 50.2 50.2 50.2 50.2 ...
##   .. ..$ time: POSIXct[1:2903], format: "2017-06-24 01:10:13" "2017-06-24 01:20:13" ...
##   ..@ coords.nrs             : num(0) 
##   ..@ coords                 : num [1:2903, 1:2] -5.11 -5.11 -5.11 -5.11 -5.11 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   ..@ bbox                   : num [1:2, 1:2] -5.12 50.14 -5.07 50.17
##   .. ..- attr(*, "dimnames")=List of 2
##   ..@ proj4string            :Formal class 'CRS' [package "sp"] with 1 slot
##   ..@ trackIdUnUsedRecords   : Factor w/ 3 levels "Ares","Athena",..: 
##   ..@ timestampsUnUsedRecords: NULL
##   ..@ sensorUnUsedRecords    : Factor w/ 2 levels "unknown","interpolateTime": 
##   ..@ dataUnUsedRecords      :'data.frame':  0 obs. of  0 variables
##   ..@ dateCreation           : POSIXct[1:1], format: "2023-03-25 21:21:29"
##   ..@ study                  : chr(0) 
##   ..@ citation               : chr(0) 
##   ..@ license                : chr(0)

———————————————————————————————-

Analyze Data

library(move)

You can use get_maptypes() get a list of all available map_services and map_types

get_maptypes()
## $osm
##  [1] "streets"      "streets_de"   "streets_fr"   "humanitarian" "topographic" 
##  [6] "roads"        "hydda"        "hydda_base"   "hike"         "grayscale"   
## [11] "no_labels"    "watercolor"   "toner"        "toner_bg"     "toner_lite"  
## [16] "terrain"      "terrain_bg"   "mtb"         
## 
## $carto
##  [1] "light"                "light_no_labels"      "light_only_labels"   
##  [4] "dark"                 "dark_no_labels"       "dark_only_labels"    
##  [7] "voyager"              "voyager_no_labels"    "voyager_only_labels" 
## [10] "voyager_labels_under"
## 
## $mapbox
##  [1] "satellite"     "streets"       "streets_basic" "hybrid"       
##  [5] "light"         "dark"          "high_contrast" "outdoors"     
##  [9] "hike"          "wheatpaste"    "pencil"        "comic"        
## [13] "pirates"       "emerald"

Creating spatial frames

frames <- frames_spatial(
  m = m, # input data
  trace_show = TRUE, # show trace of complete path
  equidistant = FALSE, # make map square (FALSE = prevent stretching of map)
  map_service = "osm", # select map service
  map_type = "streets", # select map type
  alpha = 0.75, # select transparency level for map
  path_colours = c("#D55E00", "#009E73", "#56B4E9"), # select colors for paths
  ext = extent(-5.12, -5.065, 50.14, 50.175), # define a custom extent for map
  map_res = 0.8, # select map resolution
  )
## Checking temporal alignment...
## Processing movement data...
## Approximated animation duration: ≈ 39.84s at 25 fps for 996 frames
## Retrieving and compositing basemap imagery...
## Assigning raster maps to frames...
## Creating frames...

You can use length() to check the number of frames

length(frames)
## [1] 996

You can use frames[[]] to view a single frame at a time

frames[[100]]

# Adapting spatial frames

You can use add_labels() to add labels to frames

frames <- add_labels(frames, x = "Longitude", y = "Latitude") # add axis labels

frames[[100]]

You can use add_timestamps() to show timestamps on frames

frames <- add_timestamps(frames, type = "label")

frames[[100]]

You can use add_progress() to add a progress bar to frames

frames <- add_progress(frames, colour = "red")

frames[[100]]

You can use add_northarrow() to add a north arrow to frames

frames <- add_northarrow(frames, colour = "black", height = 0.08, position = "bottomleft")

frames[[100]]

You can use add_scalebar() to add a scalebar to frames

frames <- add_scalebar(frames, colour = "black", position = "bottomright", height = 0.022, label_margin = 1.4, distance = 1)

frames[[100]]

You can include additional visual features in frames

# Make data.frame() with coordinates for vertices of a polygon
data <- data.frame(x = c(-5.11, -5.10, -5.10, -5.11, -5.11),
                   y = c(50.160, 50.160, 50.165, 50.165, 50.160))

# You can customize individual frames with geom_path()
modified_frame <- frames[[100]] + geom_path(aes(x = x, y = y), data = data, colour = "red", linetype = "dashed")

modified_frame

You can use add_gg() to customize all of the frames at once

frames <- add_gg(frames, gg = expr(geom_path(aes(x = x, y = y), data = data, color = "red", linetype = "dashed")), data = data)

frames[[100]]

You can also use add_gg() to add dynamic/animated features to frames

# create data.frame containing coordinates for polygon vertices
data <- data.frame(x = c(-5.08, -5.09, -5.09, -5.08, -5.08),
                   y = c(50.150, 50.150, 50.155, 50.155, 50.150))

# make a list from the data.frame by replicating it by the length of frames
data <- rep(list(data), length.out = length(frames))

# alter the coordinates to make them shift
data <- lapply(data, function(x){
  y <- rnorm(nrow(x)-1, mean = 0.00001, sd = 0.0001) 
  x + c(y, y[1])
})

# draw each individual polygon to each frame
frames <- add_gg(frames, gg = expr(geom_path(aes(x = x, y = y), data = data, colour = "black")), data = data)

frames[[100]]